
/* <<< COPYRIGHT START >>>
 * Copyright 2006-Present OxygenSoftwareLibrary.com
 * Licensed under the GNU Lesser General Public License.
 * http://www.gnu.org/licenses/lgpl.html
 * 
 * @author: Ugorji Dick-Nwoke
 * <<< COPYRIGHT END >>>
 */


package oxygen.tool.classcompatibilityinspector;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class CCIHandler {
  private static ClassLoader CL = CCIHandler.class.getClassLoader();
  private static int INTERNAL_API = 1;
  private static int PUBLIC_API = 2;
  private static int DEPRECATED_API = 3;
  private static int UNAVAILABLE_API = 4;
  private static int UNKNOWN_CLASSIFICATION_API = 5;
  
  // we use arrays because we can sort them, and then do searches much more efficiently
  private String[] classPrefixes;
  
  private String[] internalApis;
  private String[] publicApis;
  private String[] deprecatedApis;
  
  // class name to super class name
  private Map classesMap = new HashMap();
  // interface to array of super interfaces
  private Map interfacesMap = new HashMap();
  
  private Pattern[] classPrefixesPatterns;

  /**
   * loads up an XML class representation.
   * The InputStream is an XML stream, which holds the information
   * on the classes available, and the prefixes we know about.
   */
  public CCIHandler(InputStream is) throws Exception {
    CCIXMLParser parsehdlr = this.new CCIXMLParser();
    
    SAXParserFactory fact = SAXParserFactory.newInstance();
    SAXParser sp = fact.newSAXParser();
    
    sp.parse(is, parsehdlr);    
  }

  /**
   * Get an array of all class prefixes for this handler
   */
  public String[] getClassPrefixes() {
    return classPrefixes;
  }
  
  /**
   * Takes an array of API calls, and sees which ones match our 
   * set of prefixes, but we don't know about 
   * (i.e. is not in our list of public, deprecated or internal Apis)
   */
  public CCIResults getRefs(String[] refApis) {
    CCIResults results = new CCIResults();
    for(int i = 0; i < refApis.length; i++) {
      if(matchesAPrefix(refApis[i])) {
        int rtnval = getAPIClassification(refApis[i]);
        if(rtnval == PUBLIC_API) {
          // nothing to do
        } else if(rtnval == INTERNAL_API) {
          results.addInternalApi(refApis[i]);
        } else if(rtnval == DEPRECATED_API) {
          results.addDeprecatedApi(refApis[i]);
        } else if(rtnval == UNAVAILABLE_API) {
          results.addUnavailableApi(refApis[i]);
        } else {
          CCIUtils.say("CIHandler getRefs adding unknown_classification_api for: " + refApis[i]);
          results.addUnavailableApi(refApis[i]);          
        }
      }
    }
    //if(!results.isEmpty()) CCIUtils.say("CIHandler getRefs returning: " + results);
    return results;
  }
  
  /**
   * Recursive function
   * tells us whether an API call is public, internal, deprecated, or unavailable
   * If it cannot determine it, then the API is UNKNOWN_CLASSIFICATION_API
   */
  private int getAPIClassification(String refapi) {
    int loc = 0;
    String refapi2 = getSubstringTo(refapi, "(");
    loc = Arrays.binarySearch(publicApis, refapi2);
    if(loc >= 0) {
      return PUBLIC_API;
    }
    loc = Arrays.binarySearch(internalApis, refapi2);
    if(loc >= 0) {
      return INTERNAL_API;
    }
    loc = Arrays.binarySearch(deprecatedApis, refapi2);
    if(loc >= 0) {
      return DEPRECATED_API;
    }
    // now candidate for UNAVAILABLE
    // if a class or interface referenced directly (not as a method or variable or constructor)
    // class here refers to class or interface
    int indexOfBracket = refapi.indexOf('(');
    if(indexOfBracket == -1) {
      return UNAVAILABLE_API;
    }
    int indexOfDotAfterClassname = refapi.lastIndexOf('.', indexOfBracket);
    String classname = refapi.substring(0, indexOfDotAfterClassname);
    String membername = refapi.substring(indexOfDotAfterClassname + 1, indexOfBracket);
    // handle if class constructor
    if(classname.endsWith(membername)) {
      return getAPIClassification(classname);
    }
    // handle if class
    String superClassname = (String)classesMap.get(classname);
    if(superClassname != null) {
      return getAPIClassification(superClassname + "." + membername);
    }
    // handle if interface
    String[] superinterfaces = (String[])interfacesMap.get(classname);
    if(superinterfaces != null) {
      //CCIUtils.say("checking " + superinterfaces.length + " interfaces for: " + classname);
      for(int i = 0; i < superinterfaces.length; i++) {
        //CCIUtils.say("checking " + superinterfaces[i] + "." + membername);
        int rtnval = getAPIClassification(superinterfaces[i] + "." + membername);
        if(!(rtnval == UNAVAILABLE_API || rtnval == UNKNOWN_CLASSIFICATION_API)) {
          return rtnval;
        }
      }
    }
    // handle if the method can be found, and thus list as public
    try {
      Method[] methods = Class.forName(classname, false, CL).getMethods();
      boolean foundMethod = false;
      for(int i = 0; i < methods.length; i++) {
        if(methods[i].getName().equals(membername)) {
          foundMethod = true;
          break;
        }
      }
      if(foundMethod) {
        return PUBLIC_API;
      }
    } catch(Exception exc) {
      //ignore - could not find the method
    }
    // if all fails, return UNKNOWN_CLASSIFICATION_API
    return UNKNOWN_CLASSIFICATION_API;
  }
  
  /**
   * Helper function to see if the string matches any one of our prefixes
   */
  private boolean matchesAPrefix(String s) {
    boolean matched = false;
    for(int i = 0; i < classPrefixes.length; i++) {
      if(s.startsWith(classPrefixes[i])) {
        matched = true;
        break;
      }
    }
    return matched;
  }
  
  private String getSubstringTo(String s, String upto) {
    int i = s.indexOf(upto);
    if(i >= 0) {
      s = s.substring(0, i);
    }
    return s.trim();
  }

  public String toString() {
    return "classPrefixes: " + classPrefixes.length + 
      " internalApis: " + internalApis.length + 
      " deprecatedApis: " + deprecatedApis.length + 
      " publicApis: " + publicApis.length;      
  }
  
  private class CCIXMLParser extends DefaultHandler {
    private int IN_API_STRUCTURE = 0;
    private int IN_INTERNAL_APIS = 1;
    private int IN_DEPRECATED_APIS = 2;
    private int IN_PUBLIC_APIS = 3;
    private int IN_CLASS_PREFIXES = 4;
    
    private List classPrefixesList = new ArrayList();
    private List internalApisList = new ArrayList();
    private List publicApisList = new ArrayList();
    private List deprecatedApisList = new ArrayList();

    private StringBuffer buf = new StringBuffer(8196);
    private int state = -1;
    
    public void startDocument() throws SAXException { 
      //CCIUtils.say("CI Handler: startDocument");
    }
    
    public void endDocument() throws SAXException { 
      buf = null;
      //CCIUtils.say("CI Handler: endDocument");
      //CCIUtils.say("CI Handler: " + classPrefixesList.size() + " " + 
      //                        internalApisList.size() + " " + 
      //                        publicApisList.size() + " " + 
      //                        deprecatedApisList.size() + " ");
      classPrefixes = (String[])classPrefixesList.toArray(new String[0]);
      internalApis = (String[])internalApisList.toArray(new String[0]);
      publicApis = (String[])publicApisList.toArray(new String[0]);
      deprecatedApis = (String[])deprecatedApisList.toArray(new String[0]);
    
      Arrays.sort(classPrefixes);
      Arrays.sort(internalApis);
      Arrays.sort(publicApis);
      Arrays.sort(deprecatedApis);
    
      classPrefixesPatterns = new Pattern[classPrefixes.length];
      for(int i = 0; i < classPrefixes.length; i++) {
        classPrefixesPatterns[i] = Pattern.compile(classPrefixes[i], Pattern.DOTALL);
      }
    
      //CCIUtils.say("CI Handler: done sorting arrays et al");
    }
    
    public void startElement(String uri, String localName, String qName, 
                             Attributes attributes) throws SAXException { 
      //CCIUtils.say("CI Handler: startElement");
      if("class-prefixes".equals(qName)) {
        state = IN_CLASS_PREFIXES;
      } else if("public-apis".equals(qName)) {
        state = IN_PUBLIC_APIS;
      } else if("deprecated-apis".equals(qName)) {
        state = IN_DEPRECATED_APIS;
      } else if("internal-apis".equals(qName)) {
        state = IN_INTERNAL_APIS;
      } else if("api-structures".equals(qName)) {
        state = IN_API_STRUCTURE;
      } else if("class".equals(qName)) {
        classesMap.put(attributes.getValue("name"),
                       attributes.getValue("superclass"));
      } else if("interface".equals(qName)) {
        ArrayList list = new ArrayList();
        StringTokenizer stz = new StringTokenizer(attributes.getValue("superinterfaces"), " ,");
        while(stz.hasMoreTokens()) {
          list.add(stz.nextToken());
        }
        interfacesMap.put(attributes.getValue("name"),
                          list.toArray(new String[0]));
      }
      buf = new StringBuffer(8196);
    }
    
    public void endElement(String uri, String localName, String qName) throws SAXException { 
      //CCIUtils.say("CI Handler: endElement: buffer length: " + buf.length());
      String value = buf.toString();
      //CCIUtils.say("CI Handler: buffer: " + buf);
      //CCIUtils.say("CI Handler: localname: " + localName + " qName: " + qName + " uri: " + uri);
      if("class-prefixes".equals(qName)) {
        CCIUtils.tokenizeIntoList(value, classPrefixesList);
      } else if("public-apis".equals(qName)) {
        CCIUtils.tokenizeIntoList(value, publicApisList);
      } else if("deprecated-apis".equals(qName)) {
        CCIUtils.tokenizeIntoList(value, deprecatedApisList);
      } else if("internal-apis".equals(qName)) {
        CCIUtils.tokenizeIntoList(value, internalApisList);
      } 
    }
    
    public void characters(char[] ch, int start, int length)  throws SAXException { 
      buf.append(ch, start, length);
    }
    
  }


}

